/* ------------------------- htmlparser/fragment.js ------------------------- */
'use strict';
VED.htmlParser.fragment = new Class({
   initialize: function() {
      this.children = [];
      this.parent = null;
      this._ = {
         isBlockLike: true,
         hasInlineStarted: false
      }
   }
});

(function() {
   // Block-level elements whose internal structure should be respected during
   // parser fixing.
   var nonBreakingBlocks = VED.tools.extend( { table:1,ul:1,ol:1,dl:1 }, VED.dtd.table, VED.dtd.ul, VED.dtd.ol, VED.dtd.dl );

   var listBlocks = { ol:1,ul:1 };

   // Dtd of the fragment element, basically it accept anything except for intermediate structure, e.g. orphan <li>.
   var rootDtd = VED.tools.extend( {}, { html:1 }, VED.dtd.html, VED.dtd.body, VED.dtd.head, { style:1,script:1 } );

   function isRemoveEmpty( node ) {
      // Empty link is to be removed when empty but not anchor. (#7894)
      return node.name == 'a' && node.attributes.href || VED.dtd.$removeEmpty[ node.name ];
   }

   VED.htmlParser.fragment.fromHtml = function( fragmentHtml, parent, fixingBlock ) {
      var parser = new VED.htmlParser();

      var root = parent instanceof VED.htmlParser.element ? parent : typeof parent == 'string' ? new VED.htmlParser.element( parent ) : new VED.htmlParser.fragment();

      var pendingInline = [],
         pendingBRs = [],
         currentNode = root,
         // Indicate we're inside a <textarea> element, spaces should be touched differently.
         inTextarea = root.name == 'textarea',
         // Indicate we're inside a <pre> element, spaces should be touched differently.
         inPre = root.name == 'pre';

      function checkPending( newTagName ) {
         var pendingBRsSent;

         if( pendingInline.length > 0 ) {
            for( var i = 0; i < pendingInline.length; i++ ) {
               var pendingElement = pendingInline[ i ],
                  pendingName = pendingElement.name,
                  pendingDtd = VED.dtd[ pendingName ],
                  currentDtd = currentNode.name && VED.dtd[ currentNode.name ];

               if( ( !currentDtd || currentDtd[ pendingName ] ) && ( !newTagName || !pendingDtd || pendingDtd[ newTagName ] || !VED.dtd[ newTagName ] ) ) {
                  if( !pendingBRsSent ) {
                     sendPendingBRs();
                     pendingBRsSent = 1;
                  }

                  // Get a clone for the pending element.
                  pendingElement = pendingElement.clone();

                  // Add it to the current node and make it the current,
                  // so the new element will be added inside of it.
                  pendingElement.parent = currentNode;
                  currentNode = pendingElement;

                  // Remove the pending element (back the index by one
                  // to properly process the next entry).
                  pendingInline.splice( i, 1 );
                  i--;
               } else {
                  // Some element of the same type cannot be nested, flat them,
                  // e.g. <a href="#">foo<a href="#">bar</a></a>. (#7894)
                  if( pendingName == currentNode.name )
                     addElement( currentNode, currentNode.parent, 1 ), i--;
               }
            }
         }
      }

      function sendPendingBRs() {
         while ( pendingBRs.length )
            addElement( pendingBRs.shift(), currentNode );
      }

      // Rtrim empty spaces on block end boundary. (#3585)
      function removeTailWhitespace( element ) {
         if( element._.isBlockLike && element.name != 'pre' && element.name != 'textarea' ) {
            var length = element.children.length,
               lastChild = element.children[ length - 1 ],
               text;
            if( lastChild && lastChild.type == 3 ) {
               if( !( text = lastChild.value.rtrim() ) )
                  element.children.length = length - 1;
               else
                  lastChild.value = text;
            }
         }
      }

      function addElement( element, target, moveCurrent ) {
         target = target || currentNode || root;

         // Current element might be mangled by fix body below,
         // save it for restore later.
         var savedCurrent = currentNode;

         // Ignore any element that has already been added.
         if( element.previous === undefined ) {
            if( checkAutoParagraphing( target, element ) ) {
               // Create a <p> in the fragment.
               currentNode = target;
               parser.onTagOpen( fixingBlock, {} );

               // The new target now is the <p>.
               element.returnPoint = target = currentNode;
            }

            removeTailWhitespace( element );

            // Avoid adding empty inline.
            if( !( isRemoveEmpty( element ) && !element.children.length ) )
               target.add( element );

            if( element.name == 'pre' )
               inPre = false;

            if( element.name == 'textarea' )
               inTextarea = false;
         }

         if( element.returnPoint ) {
            currentNode = element.returnPoint;
            delete element.returnPoint;
         } else
            currentNode = moveCurrent ? target : savedCurrent;
      }

      // Auto paragraphing should happen when inline content enters the root element.
      function checkAutoParagraphing( parent, node ) {

         // Check for parent that can contain block.
         if( ( parent == root || parent.name == 'body' ) && fixingBlock &&
             ( !parent.name || VED.dtd[ parent.name ][ fixingBlock ] )
         ) {
            var name, realName;
            if( node.attributes && ( realName = node.attributes[ 'data-ved-real-element-type' ] ) )
               name = realName;
            else
               name = node.name;

            // Text node, inline elements are subjected, except for <script>/<style>.
            return name && name in VED.dtd.$inline &&
                   !( name in VED.dtd.head ) &&
                   !node.isOrphan ||
                   node.type == 3;
         }
      }

      // Judge whether two element tag names are likely the siblings from the same
      // structural element.
      function possiblySibling( tag1, tag2 ) {
         if( tag1 in VED.dtd.$listItem || tag1 in VED.dtd.$tableContent )
            return tag1 == tag2 || tag1 == 'dt' && tag2 == 'dd' || tag1 == 'dd' && tag2 == 'dt';

         return false;
      }

      parser.onTagOpen = function( tagName, attributes, selfClosing, optionalClose ) {
         var element = new VED.htmlParser.element( tagName, attributes );

         // "isEmpty" will be always "false" for unknown elements, so we
         // must force it if the parser has identified it as a selfClosing tag.
         if( element.isUnknown && selfClosing )
            element.isEmpty = true;

         // Check for optional closed elements, including browser quirks and manually opened blocks.
         element.isOptionalClose = optionalClose;

         // This is a tag to be removed if empty, so do not add it immediately.
         if( isRemoveEmpty( element ) ) {
            pendingInline.push( element );
            return;
         } else if( tagName == 'pre' )
            inPre = true;
         else if( tagName == 'br' && inPre ) {
            currentNode.add( new VED.htmlParser.text( '\n' ) );
            return;
         } else if( tagName == 'textarea' )
            inTextarea = true;

         if( tagName == 'br' ) {
            pendingBRs.push( element );
            return;
         }

         while( 1 ) {
            var currentName = currentNode.name;
            var currentDtd = currentName ? ( VED.dtd[ currentName ] || ( currentNode._.isBlockLike ? VED.dtd.div : VED.dtd.span ) ) : rootDtd;

            // If the element cannot be child of the current element.
            if( !element.isUnknown && !currentNode.isUnknown && !currentDtd[ tagName ] ) {
               // Current node doesn't have a close tag, time for a close
               // as this element isn't fit in. (#7497)
               if( currentNode.isOptionalClose )
                  parser.onTagClose( currentName );
               // Fixing malformed nested lists by moving it into a previous list item. (#3828)
               else if( tagName in listBlocks && currentName in listBlocks ) {
                  var children = currentNode.children,
                     lastChild = children[ children.length - 1 ];

                  // Establish the list item if it's not existed.
                  if( !( lastChild && lastChild.name == 'li' ) )
                     addElement( ( lastChild = new VED.htmlParser.element( 'li' ) ), currentNode );

                  !element.returnPoint && ( element.returnPoint = currentNode );
                  currentNode = lastChild;
               }
               // Establish new list root for orphan list items, but NOT to create
               // new list for the following ones, fix them instead. (#6975)
               // <dl><dt>foo<dd>bar</dl>
               // <ul><li>foo<li>bar</ul>
               else if( tagName in VED.dtd.$listItem &&
                     !possiblySibling( tagName, currentName ) ) {
                  parser.onTagOpen( tagName == 'li' ? 'ul' : 'dl', {}, 0, 1 );
               }
               // We're inside a structural block like table and list, AND the incoming element
               // is not of the same type (e.g. <td>td1<td>td2</td>), we simply add this new one before it,
               // and most importantly, return back to here once this element is added,
               // e.g. <table><tr><td>td1</td><p>p1</p><td>td2</td></tr></table>
               else if( currentName in nonBreakingBlocks &&
                     !possiblySibling( tagName, currentName ) ) {
                  !element.returnPoint && ( element.returnPoint = currentNode );
                  currentNode = currentNode.parent;
               } else {
                  // The current element is an inline element, which
                  // need to be continued even after the close, so put
                  // it in the pending list.
                  if( currentName in VED.dtd.$inline )
                     pendingInline.unshift( currentNode );

                  // The most common case where we just need to close the
                  // current one and append the new one to the parent.
                  if( currentNode.parent )
                     addElement( currentNode, currentNode.parent, 1 );
                  // We've tried our best to fix the embarrassment here, while
                  // this element still doesn't find it's parent, mark it as
                  // orphan and show our tolerance to it.
                  else {
                     element.isOrphan = 1;
                     break;
                  }
               }
            } else
               break;
         }

         checkPending( tagName );
         sendPendingBRs();

         element.parent = currentNode;

         if( element.isEmpty )
            addElement( element );
         else
            currentNode = element;
      };

      parser.onTagClose = function( tagName ) {
         // Check if there is any pending tag to be closed.
         for ( var i = pendingInline.length - 1; i >= 0; i-- ) {
            // If found, just remove it from the list.
            if( tagName == pendingInline[ i ].name ) {
               pendingInline.splice( i, 1 );
               return;
            }
         }

         var pendingAdd = [],
            newPendingInline = [],
            candidate = currentNode;

         while ( candidate != root && candidate.name != tagName ) {
            // If this is an inline element, add it to the pending list, if we're
            // really closing one of the parents element later, they will continue
            // after it.
            if( !candidate._.isBlockLike )
               newPendingInline.unshift( candidate );

            // This node should be added to it's parent at this point. But,
            // it should happen only if the closing tag is really closing
            // one of the nodes. So, for now, we just cache it.
            pendingAdd.push( candidate );

            // Make sure return point is properly restored.
            candidate = candidate.returnPoint || candidate.parent;
         }

         if( candidate != root ) {
            // Add all elements that have been found in the above loop.
            for ( i = 0; i < pendingAdd.length; i++ ) {
               var node = pendingAdd[ i ];
               addElement( node, node.parent );
            }

            currentNode = candidate;

            if( candidate._.isBlockLike )
               sendPendingBRs();

            addElement( candidate, candidate.parent );

            // The parent should start receiving new nodes now, except if
            // addElement changed the currentNode.
            if( candidate == currentNode )
               currentNode = currentNode.parent;

            pendingInline = pendingInline.concat( newPendingInline );
         }

         if( tagName == 'body' )
            fixingBlock = false;
      };

      parser.onText = function( text ) {
         // Trim empty spaces at beginning of text contents except <pre> and <textarea>.
         if( ( !currentNode._.hasInlineStarted || pendingBRs.length ) && !inPre && !inTextarea ) {
            text = text.ltrim();

            if( text.length === 0 )
               return;
         }

         var currentName = currentNode.name,
            currentDtd = currentName ? ( VED.dtd[ currentName ] || ( currentNode._.isBlockLike ? VED.dtd.div : VED.dtd.span ) ) : rootDtd;

         // Fix orphan text in list/table. (#8540) (#8870)
         if( !inTextarea && !currentDtd[ '#' ] && currentName in nonBreakingBlocks ) {
            parser.onTagOpen( currentName in listBlocks ? 'li' : currentName == 'dl' ? 'dd' : currentName == 'table' ? 'tr' : currentName == 'tr' ? 'td' : '' );
            parser.onText( text );
            return;
         }

         sendPendingBRs();
         checkPending();

         // Shrinking consequential spaces into one single for all elements
         // text contents.
         if( !inPre && !inTextarea )
            text = text.replace( /[\t\r\n ]{2,}|[\t\r\n]/g, ' ' );

         text = new VED.htmlParser.text( text );


         if( checkAutoParagraphing( currentNode, text ) )
            this.onTagOpen( fixingBlock, {}, 0, 1 );

         currentNode.add( text );
      };

      parser.onCDATA = function( cdata ) {
         currentNode.add( new VED.htmlParser.cdata( cdata ) );
      };

      parser.onComment = function( comment ) {
         sendPendingBRs();
         checkPending();
         currentNode.add( new VED.htmlParser.comment( comment ) );
      };

      // Parse it.
      parser.parse( fragmentHtml );

      // Send all pending BRs except one, which we consider a unwanted bogus. (#5293)
      sendPendingBRs( !VED.env.ie && 1 );

      // Close all pending nodes, make sure return point is properly restored.
      while ( currentNode != root )
         addElement( currentNode, currentNode.parent, 1 );

      removeTailWhitespace( root );

      return root;
   };

   VED.htmlParser.fragment.implement({

      type: 11,

      add: function( node, index ) {
         isNaN( index ) && ( index = this.children.length );

         var previous = index > 0 ? this.children[ index - 1 ] : null;
         if( previous ) {
            // If the block to be appended is following text, trim spaces at
            // the right of it.
            if( node._.isBlockLike && previous.type == 3 ) {
               previous.value = previous.value.rtrim();

               // If we have completely cleared the previous node.
               if( previous.value.length === 0 ) {
                  // Remove it from the list and add the node again.
                  this.children.pop();
                  this.add( node );
                  return;
               }
            }

            previous.next = node;
         }

         node.previous = previous;
         node.parent = this;

         this.children.splice( index, 0, node );

         if( !this._.hasInlineStarted )
            this._.hasInlineStarted = node.type == 3 || ( node.type == 1 && !node._.isBlockLike );
      },

      filter: function( filter ) {
         // Apply the root filter.
         filter.onRoot( this );

         this.filterChildren( filter );
      },

      filterChildren: function( filter, filterRoot ) {
         // If this element's children were already filtered
         // by current filter, don't filter them 2nd time.
         // This situation may occur when filtering bottom-up
         // (filterChildren() called manually in element's filter),
         // or in unpredictable edge cases when filter
         // is manipulating DOM structure.
         if ( this.childrenFilteredBy == filter.id )
            return;

         // Filtering root if enforced.
         if ( filterRoot && !this.parent )
            filter.onRoot( this );

         this.childrenFilteredBy = filter.id;

         // Don't cache anything, children array may be modified by filter rule.
         for ( var i = 0; i < this.children.length; i++ ) {
            // Stay in place if filter returned false, what means
            // that node has been removed.
            if ( this.children[ i ].filter( filter ) === false )
               i--;
         }
      },

      writeHtml: function( writer, filter ) {
         if ( filter )
            this.filter( filter );

         this.writeChildrenHtml( writer );
      },

      writeChildrenHtml: function( writer, filter, filterRoot ) {

         // Filtering root if enforced.
         if( filterRoot && !this.parent && filter )
            filter.onRoot( this );

         if ( filter )
            this.filterChildren( filter );

         for ( var i = 0, children = this.children, l = children.length; i < l; i++ )
            children[ i ].writeHtml( writer );
      },

      forEach: function( callback, type, skipRoot ) {
         if ( !skipRoot && ( !type || this.type == type ) )
            callback( this );

         var children = this.children,
            node,
            i = 0,
            l = children.length;

         for ( ; i < l; i++ ) {
            node = children[ i ];
            if ( node.type == VED.NODE_ELEMENT )
               node.forEach( callback, type );
            else if ( !type || node.type == type )
               callback( node );
         }
      }
   });
})();





